﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Web;

namespace AjaxControlFramework.Reflection
{
    public delegate object MethodInvoke(object instance, object[] parameters);



    public static class MethodCache
    {
        //------// Fields \\------------------------------------------------\\
        private static object InternalMethodCollectionLock = new object();


        private static ReflectiveCacheCollection<MethodCacheEntry> InternalMethodCollection
        {
            get
            {
                ReflectiveCacheCollection<MethodCacheEntry> collection = (ReflectiveCacheCollection<MethodCacheEntry>)HttpContext.Current.Cache.Get("AjaxControlFramework.Reflection.MethodCache.InternalMethodCollection");

                if (collection == null)
                {
                    collection = new ReflectiveCacheCollection<MethodCacheEntry>();
                    HttpContext.Current.Cache.Insert("AjaxControlFramework.Reflection.MethodCache.InternalMethodCollection", collection, null, System.Web.Caching.Cache.NoAbsoluteExpiration, new TimeSpan(0, 20, 0));
                }

                return collection;
            }
        }
        //------\\ Fields //------------------------------------------------//



        //------// Methods \\-----------------------------------------------\\
        /// <returns>Returns true if the method successfully resolved and was invoked, false otherwise.</returns>
        public static bool InvokeMethod<T>(object instance, string methodName, out object returnValue, object[] parameters)
        {
            return InvokeMethod(instance, typeof(T), methodName, out returnValue, parameters);
        }


        /// <returns>Returns true if the method successfully resolved and was invoked, false otherwise.</returns>
        public static bool InvokeMethod(object instance, Type instanceType, string methodName, out object returnValue, object[] parameters)
        {
            return InvokeMethod(instance, instanceType, methodName, out returnValue, parameters, null);
        }


        /// <returns>Returns true if the method successfully resolved and was invoked, false otherwise.</returns>
        public static bool InvokeMethod(object instance, Type instanceType, string methodName, out object returnValue, object[] parameters, AuthenticationStrategy authenticationStrategy)
        {
            if (instanceType == null)
            {
                if (instance == null) { throw new ArgumentNullException("instanceType", "When the \"instance\" parameter is null, the method to be invoked is considered to be static. In these cases where \"instance\" is null, the \"instanceType\" parameter may not be null."); }
                else { throw new ArgumentNullException("instanceType"); }
            }


            MethodCacheEntry cacheEntry = null;

            lock (InternalMethodCollectionLock)
            {
                cacheEntry = InternalMethodCollection[instanceType, methodName];
            }

            if (cacheEntry != null)
            {
                if (cacheEntry.Delegate == null)
                {
                    returnValue = null;
                    return false;
                }
                else
                {
                    if (cacheEntry.Attribute.Protected && authenticationStrategy != null && (!authenticationStrategy.Authenticated || !authenticationStrategy.UserIsAuthorized(cacheEntry.Attribute.AuthorizedRoles)))
                    {
                        returnValue = null;
                        return false;
                    }
                    else
                    {
                        returnValue = cacheEntry.Delegate(instance, parameters);
                        return true;
                    }
                }
            }
            else
            {
                // Check to see if the method collection cache for the entire instance type has expired.
                if (InternalMethodCollection[instanceType] == null)
                {
                    // If nothing is in the cache for the instance type, re-query for AjaxControlMethods and cache them...
                    CacheMethodsOfType(instanceType);


                    // ...then check again for the desired method in the newly cached collection of AjaxControlMethods.
                    lock (InternalMethodCollectionLock)
                    {
                        cacheEntry = InternalMethodCollection[instanceType, methodName];
                    }

                                        
                    if (cacheEntry != null)
                    {
                        // If the cached method can now be found, attempt to invoke it.
                        if (cacheEntry.Delegate == null)
                        {
                            returnValue = null;
                            return false;
                        }
                        else
                        {
                            if (cacheEntry.Attribute.Protected && authenticationStrategy != null && (!authenticationStrategy.Authenticated || !authenticationStrategy.UserIsAuthorized(cacheEntry.Attribute.AuthorizedRoles)))
                            {
                                returnValue = null;
                                return false;
                            }
                            else
                            {
                                returnValue = cacheEntry.Delegate(instance, parameters);
                                return true;
                            }
                        }
                    }
                    else
                    {
                        // If it can't be found (yet again) then bail out.
                        returnValue = null;
                        return false;
                    }
                }
                else
                {
                    MethodInvoke method = CacheMethod(instanceType, methodName, GetParameterTypes(parameters));

                    if (method == null)
                    {
                        returnValue = null;
                        return false;
                    }
                    else
                    {
                        if (cacheEntry.Attribute.Protected && authenticationStrategy != null && (!authenticationStrategy.Authenticated || !authenticationStrategy.UserIsAuthorized(cacheEntry.Attribute.AuthorizedRoles)))
                        {
                            returnValue = null;
                            return false;
                        }
                        else
                        {
                            returnValue = method(instance, parameters);
                            return true;
                        }
                    }
                }
            }
        }


        private static Type[] GetParameterTypes(object[] parameters)
        {
            Type[] parameterTypes = new Type[parameters.Length];

            for (int index = 0; index < parameters.Length; index++)
            {
                parameterTypes[index] = parameters[index].GetType();
            }

            return parameterTypes;
        }


        public static MethodInvoke CacheMethod(Type instanceType, string methodName, params Type[] parameterTypes)
        {
            if (instanceType == null) { throw new ArgumentNullException("instanceType"); }

            MethodInfo methodInfo = instanceType.GetMethod(methodName, BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance, null, parameterTypes, null);
            if (methodInfo == null) { throw new InvalidOperationException(String.Format("A method of the name {0} and parameter types provided could not be found.", methodName)); }

            AjaxControlMethodAttribute methodAttribute = (AjaxControlMethodAttribute)methodInfo.GetCustomAttributes(typeof(AjaxControlMethodAttribute), true).FirstOrDefault();

            return CacheMethod(instanceType, methodAttribute, methodInfo);
        }


        public static MethodInvoke CacheMethod(Type instanceType, string actualMethodName, string renderedMethodName, params Type[] parameterTypes)
        {
            if (instanceType == null) { throw new ArgumentNullException("instanceType"); }

            MethodInfo methodInfo = instanceType.GetMethod(actualMethodName, BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance, null, parameterTypes, null);
            if (methodInfo == null) { throw new InvalidOperationException(String.Format("A method of the name {0} and parameter types provided could not be found.", actualMethodName)); }

            return CacheMethod(instanceType, new AjaxControlMethodAttribute(renderedMethodName), methodInfo);
        }


        public static MethodInvoke CacheMethod(Type instanceType, AjaxControlMethodAttribute methodAttribute, MethodInfo methodInfo)
        {
            if (instanceType == null) { throw new ArgumentNullException("instanceType"); }
            if (methodAttribute == null) { throw new ArgumentNullException("methodAttribute"); }
            if (methodInfo == null) { throw new ArgumentNullException("methodInfo"); }

            string methodName = (methodAttribute.Name == null || methodAttribute.Name.Trim().Length == 0 ? methodInfo.Name : methodAttribute.Name.Trim());


            // Locking isn't going any further than this. Won't be checking again after the lock to see if the entry is STILL not in the 
            // internal collection as I want to allow for overwritting of cache entries.
            lock (InternalMethodCollectionLock)
            {
                MethodCacheEntry cacheEntry = new MethodCacheEntry(methodName, methodAttribute);

                ParameterInfo[] parameters = methodInfo.GetParameters();
                DynamicMethod dynamicMethod = new DynamicMethod(instanceType.Name + "_" + methodName, typeof(object), new Type[] { typeof(object), typeof(object[]) }, true);

                ILGenerator ilGenerator = dynamicMethod.GetILGenerator();

                ilGenerator.Emit(OpCodes.Ldarg_0);
                ilGenerator.Emit(OpCodes.Castclass, instanceType);

                for (int paramIndex = 0; paramIndex < parameters.Length; paramIndex++)
                {
                    ilGenerator.Emit(OpCodes.Ldarg_1);
                    ilGenerator.Emit(OpCodes.Ldc_I4, paramIndex);
                    ilGenerator.Emit(OpCodes.Ldelem_Ref);

                    if (parameters[paramIndex].ParameterType.IsValueType)
                    {
                        ilGenerator.Emit(OpCodes.Unbox_Any, parameters[paramIndex].ParameterType);
                    }
                    else
                    {
                        ilGenerator.Emit(OpCodes.Castclass, parameters[paramIndex].ParameterType);
                    }
                }

                if (methodInfo.IsVirtual)
                {
                    ilGenerator.Emit(OpCodes.Callvirt, methodInfo);
                }
                else
                {
                    ilGenerator.Emit(OpCodes.Call, methodInfo);
                }

                if (methodInfo.ReturnType.Name == "Void")
                {
                    ilGenerator.Emit(OpCodes.Ldnull);
                }
                else if (methodInfo.ReturnType.IsValueType)
                {
                    ilGenerator.Emit(OpCodes.Box, methodInfo.ReturnType);
                }

                ilGenerator.Emit(OpCodes.Ret);


                MethodInvoke method = (MethodInvoke)dynamicMethod.CreateDelegate(typeof(MethodInvoke));

                if (method == null)
                {
                    InternalMethodCollection[instanceType, methodName] = cacheEntry;
                    return null;
                }

                cacheEntry.Info = methodInfo;
                cacheEntry.Delegate = method;

                InternalMethodCollection[instanceType, methodName] = cacheEntry;
                return method;
            }
        }


        public static void CacheMethodsOfType(Type instanceType)
        {
            // The following statement doesn't always work. The hierarchy tree needs to be scanned as private methods cannot be picked up within base classes by GetMethods. 
            MethodInfo[] methods = instanceType.GetMethods(BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);

            foreach (MethodInfo method in methods)
            {
                AjaxControlMethodAttribute methodAttribute = (AjaxControlMethodAttribute)method.GetCustomAttributes(typeof(AjaxControlMethodAttribute), true).FirstOrDefault();

                if (methodAttribute != null)
                {
                    CacheMethod(instanceType, methodAttribute, method);
                }
            }
        }


        public static Dictionary<string, MethodCacheEntry> GetMethodsOfType(Type instanceType)
        {
            Dictionary<string, MethodCacheEntry> methodEntries = InternalMethodCollection[instanceType];

            if (methodEntries == null)
            {
                CacheMethodsOfType(instanceType);
                methodEntries = InternalMethodCollection[instanceType];
            }

            if (methodEntries == null) // If the collection is STILL null...
            {
                methodEntries = new Dictionary<string, MethodCacheEntry>(); // ...initialize the collection.
                InternalMethodCollection[instanceType] = methodEntries;
            }

            return methodEntries;
        }
        //------\\ Methods //-----------------------------------------------//
    }
}
